package com.alphawallet.app.viewmodel;

import static com.alphawallet.app.C.EXTRA_ADDRESS;
import static com.alphawallet.app.repository.TokensRealmSource.ADDRESS_FORMAT;
import static com.alphawallet.app.widget.CopyTextView.KEY_ADDRESS;
import static com.alphawallet.token.tools.TokenDefinition.NO_SCRIPT;
import static com.alphawallet.token.tools.TokenDefinition.UNCHANGED_SCRIPT;

import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Pair;
import android.view.View;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;

import com.alphawallet.app.C;
import com.alphawallet.app.R;
import com.alphawallet.app.entity.ContractType;
import com.alphawallet.app.entity.EasAttestation;
import com.alphawallet.app.entity.QRResult;
import com.alphawallet.app.entity.Wallet;
import com.alphawallet.app.entity.WalletType;
import com.alphawallet.app.entity.analytics.QrScanSource;
import com.alphawallet.app.entity.attestation.ImportAttestation;
import com.alphawallet.app.entity.nftassets.NFTAsset;
import com.alphawallet.app.entity.tokendata.TokenGroup;
import com.alphawallet.app.entity.tokens.Attestation;
import com.alphawallet.app.entity.tokens.Token;
import com.alphawallet.app.entity.tokens.TokenCardMeta;
import com.alphawallet.app.entity.tokens.TokenInfo;
import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem;
import com.alphawallet.app.interact.ChangeTokenEnableInteract;
import com.alphawallet.app.interact.FetchTokensInteract;
import com.alphawallet.app.interact.GenericWalletInteract;
import com.alphawallet.app.repository.CoinbasePayRepository;
import com.alphawallet.app.repository.OnRampRepositoryType;
import com.alphawallet.app.repository.PreferenceRepositoryType;
import com.alphawallet.app.repository.TokensMappingRepositoryType;
import com.alphawallet.app.repository.TokensRealmSource;
import com.alphawallet.app.repository.WalletItem;
import com.alphawallet.app.repository.entity.RealmAttestation;
import com.alphawallet.app.repository.entity.RealmToken;
import com.alphawallet.app.router.CoinbasePayRouter;
import com.alphawallet.app.router.ManageWalletsRouter;
import com.alphawallet.app.router.MyAddressRouter;
import com.alphawallet.app.router.TokenDetailRouter;
import com.alphawallet.app.service.AnalyticsServiceType;
import com.alphawallet.app.service.AssetDefinitionService;
import com.alphawallet.app.service.RealmManager;
import com.alphawallet.app.service.TokensService;
import com.alphawallet.app.ui.NameThisWalletActivity;
import com.alphawallet.app.ui.QRScanning.QRScannerActivity;
import com.alphawallet.app.ui.TokenManagementActivity;
import com.alphawallet.app.walletconnect.AWWalletConnectClient;
import com.alphawallet.app.widget.WalletFragmentActionsView;
import com.alphawallet.token.entity.AttestationValidationStatus;
import com.alphawallet.token.tools.TokenDefinition;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.gson.Gson;

import org.jetbrains.annotations.NotNull;
import org.web3j.crypto.Keys;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;

import dagger.hilt.android.lifecycle.HiltViewModel;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import io.realm.Realm;
import io.realm.RealmResults;
import timber.log.Timber;

@HiltViewModel
public class WalletViewModel extends BaseViewModel
{
    public static long BALANCE_BACKUP_CHECK_INTERVAL = 5 * DateUtils.MINUTE_IN_MILLIS;
    public static double VALUE_THRESHOLD = 200.0; //$200 USD value is difference between red and grey backup warnings
    private final MutableLiveData<TokenCardMeta[]> tokens = new MutableLiveData<>();
    private final MutableLiveData<TokenCardMeta[]> updatedTokens = new MutableLiveData<>();
    private final MutableLiveData<Wallet> defaultWallet = new MutableLiveData<>();
    private final MutableLiveData<GenericWalletInteract.BackupLevel> backupEvent = new MutableLiveData<>();
    private final MutableLiveData<Pair<Double, Double>> fiatValues = new MutableLiveData<>();
    private final MutableLiveData<Token[]> tokensToRemove = new MutableLiveData<>();
    private final FetchTokensInteract fetchTokensInteract;
    private final TokenDetailRouter tokenDetailRouter;
    private final GenericWalletInteract genericWalletInteract;
    private final AssetDefinitionService assetDefinitionService;
    private final TokensService tokensService;
    private final ChangeTokenEnableInteract changeTokenEnableInteract;
    private final PreferenceRepositoryType preferenceRepository;
    private final MyAddressRouter myAddressRouter;
    private final CoinbasePayRouter coinbasePayRouter;
    private final ManageWalletsRouter manageWalletsRouter;
    private final RealmManager realmManager;
    private final OnRampRepositoryType onRampRepository;
    private long lastBackupCheck = 0;
    private long lastTokenFetchTime = 0;
    private BottomSheetDialog dialog;
    private final AWWalletConnectClient awWalletConnectClient;
    @Nullable
    private Disposable balanceUpdateCheck;

    @Inject
    WalletViewModel(
            FetchTokensInteract fetchTokensInteract,
            TokenDetailRouter tokenDetailRouter,
            GenericWalletInteract genericWalletInteract,
            AssetDefinitionService assetDefinitionService,
            TokensService tokensService,
            ChangeTokenEnableInteract changeTokenEnableInteract,
            MyAddressRouter myAddressRouter,
            CoinbasePayRouter coinbasePayRouter,
            ManageWalletsRouter manageWalletsRouter,
            PreferenceRepositoryType preferenceRepository,
            RealmManager realmManager,
            OnRampRepositoryType onRampRepository,
            AnalyticsServiceType analyticsService,
            AWWalletConnectClient awWalletConnectClient)
    {
        this.fetchTokensInteract = fetchTokensInteract;
        this.tokenDetailRouter = tokenDetailRouter;
        this.genericWalletInteract = genericWalletInteract;
        this.assetDefinitionService = assetDefinitionService;
        this.tokensService = tokensService;
        this.changeTokenEnableInteract = changeTokenEnableInteract;
        this.myAddressRouter = myAddressRouter;
        this.coinbasePayRouter = coinbasePayRouter;
        this.manageWalletsRouter = manageWalletsRouter;
        this.preferenceRepository = preferenceRepository;
        this.realmManager = realmManager;
        this.onRampRepository = onRampRepository;
        this.awWalletConnectClient = awWalletConnectClient;
        setAnalyticsService(analyticsService);
    }

    public LiveData<TokenCardMeta[]> tokens()
    {
        return tokens;
    }

    public LiveData<TokenCardMeta[]> onUpdatedTokens()
    {
        return updatedTokens;
    }

    public LiveData<Wallet> defaultWallet()
    {
        return defaultWallet;
    }

    public LiveData<GenericWalletInteract.BackupLevel> backupEvent()
    {
        return backupEvent;
    }

    public LiveData<Pair<Double, Double>> onFiatValues()
    {
        return fiatValues;
    }

    public LiveData<Token[]> removeDisplayTokens()
    {
        return tokensToRemove;
    }

    public String getWalletAddr()
    {
        return defaultWallet.getValue() != null ? defaultWallet.getValue().address : "";
    }

    public WalletType getWalletType()
    {
        return defaultWallet.getValue() != null ? defaultWallet.getValue().type : WalletType.KEYSTORE;
    }

    public void prepare()
    {
        lastTokenFetchTime = 0;
        lastBackupCheck = System.currentTimeMillis() - BALANCE_BACKUP_CHECK_INTERVAL + 5 * DateUtils.SECOND_IN_MILLIS;
        //load the activity meta list
        disposable = genericWalletInteract
                .find()
                .subscribe(this::onDefaultWallet, this::onError);
    }

    public void reloadTokens()
    {
        lastTokenFetchTime = 0;
        tokensService.startUpdateCycle();
        if (defaultWallet.getValue() != null)
        {
            fetchTokens(defaultWallet().getValue());
        }
        else
        {
            prepare();
        }
    }

    private void onDefaultWallet(Wallet wallet)
    {
        tokensService.setCurrentAddress(wallet.address);
        assetDefinitionService.startEventListener();
        defaultWallet.postValue(wallet);
        fetchTokens(wallet);
    }

    private void fetchTokens(Wallet wallet)
    {
        disposable =
                fetchTokensInteract.fetchTokenMetas(wallet, tokensService.getNetworkFilters(), assetDefinitionService)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(this::onTokenMetas, this::onError);
    }

    private void onTokenMetas(TokenCardMeta[] metaTokens)
    {
        lastTokenFetchTime = System.currentTimeMillis();
        tokens.postValue(metaTokens);
        tokensService.updateTickers();
    }

    public void searchTokens(String search)
    {
        disposable =
                fetchTokensInteract.searchTokenMetas(defaultWallet.getValue(), tokensService.getNetworkFilters(), search)
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(this::onTokenMetas, this::onError);
    }

    public void startUpdateListener()
    {
        if (balanceUpdateCheck == null || balanceUpdateCheck.isDisposed())
        {
            balanceUpdateCheck = Observable.interval(2, 2, TimeUnit.SECONDS) //check every 2 seconds for new tokens
                    .doOnNext(l -> checkTokenUpdates()).subscribe();
        }
    }

    public void stopUpdateListener()
    {
        if (balanceUpdateCheck != null && !balanceUpdateCheck.isDisposed())
        {
            balanceUpdateCheck.dispose();
            balanceUpdateCheck = null;
        }
    }

    private void checkTokenUpdates()
    {
        if (defaultWallet.getValue() == null) return;

        disposable = getUpdatedTokenMetas()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(updatedTokens::postValue, this::onError);
    }

    public AssetDefinitionService getAssetDefinitionService()
    {
        return assetDefinitionService;
    }

    public TokensService getTokensService()
    {
        return tokensService;
    }

    public Token getTokenFromService(@NotNull Token token)
    {
        Token serviceToken = tokensService.getToken(token.tokenInfo.chainId, token.getAddress());
        if (serviceToken != null && serviceToken.isEthereum())
        {
            return tokensService.getServiceToken(token.tokenInfo.chainId);
        }
        else
        {
            return (serviceToken != null) ? serviceToken : token;
        }
    }

    public Wallet getWallet()
    {
        return defaultWallet.getValue();
    }

    public void setKeyBackupTime(String walletAddr)
    {
        genericWalletInteract.updateBackupTime(walletAddr);
    }

    public void setKeyWarningDismissTime(String walletAddr)
    {
        genericWalletInteract.updateWarningTime(walletAddr);
    }

    public void setTokenEnabled(Token token, boolean enabled)
    {
        changeTokenEnableInteract.setEnable(defaultWallet.getValue(), token.getContractAddress(), enabled);
        token.tokenInfo.isEnabled = enabled;
    }

    public void showBuyEthOptions(Activity activity)
    {
        coinbasePayRouter.buyFromSelectedChain(activity, CoinbasePayRepository.Blockchains.ETHEREUM);
    }

    public void showMyAddress(Activity context)
    {
        // show bottomsheet dialog
        WalletFragmentActionsView actionsView = new WalletFragmentActionsView(context);
        actionsView.setOnCopyWalletAddressClickListener(v -> {
            dialog.dismiss();
            ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
            ClipData clip = ClipData.newPlainText(KEY_ADDRESS, Keys.toChecksumAddress(getWalletAddr()));
            if (clipboard != null)
            {
                clipboard.setPrimaryClip(clip);
            }

            Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
        });
        actionsView.setOnShowMyWalletAddressClickListener(v -> {
            dialog.dismiss();
            myAddressRouter.open(context, defaultWallet.getValue());
        });
        actionsView.setOnAddHideTokensClickListener(v -> {
            dialog.dismiss();
            Intent intent = new Intent(context, TokenManagementActivity.class);
            intent.putExtra(EXTRA_ADDRESS, getWalletAddr());
            context.startActivityForResult(intent, C.ADDED_TOKEN_RETURN);
        });
        actionsView.setOnRenameThisWalletClickListener(v -> {
            dialog.dismiss();
            Intent intent = new Intent(context, NameThisWalletActivity.class);
            context.startActivity(intent);
        });

        dialog = new BottomSheetDialog(context);
        dialog.setContentView(actionsView);
        dialog.setCancelable(true);
        dialog.setCanceledOnTouchOutside(true);
        BottomSheetBehavior<?> behavior = BottomSheetBehavior.from((View) actionsView.getParent());
        dialog.setOnShowListener(dialog -> behavior.setPeekHeight(actionsView.getHeight()));
        dialog.show();
    }

    public void showQRCodeScanning(Activity activity)
    {
        Intent intent = new Intent(activity, QRScannerActivity.class);
        intent.putExtra(C.EXTRA_UNIVERSAL_SCAN, true);
        intent.putExtra(QrScanSource.KEY, QrScanSource.WALLET_SCREEN.getValue());
        activity.startActivityForResult(intent, C.REQUEST_UNIVERSAL_SCAN);
    }

    public TokenGroup getTokenGroup(long chainId, String address)
    {
        return tokensService.getTokenGroup(tokensService.getToken(chainId, address));
    }

    public void showTokenDetail(Activity activity, Token token)
    {
        boolean hasDefinition = assetDefinitionService.hasDefinition(token);
        switch (token.getInterfaceSpec())
        {
            case ETHEREUM:
            case ERC20:
            case CURRENCY:
            case DYNAMIC_CONTRACT:
            case LEGACY_DYNAMIC_CONTRACT:
            case ETHEREUM_INVISIBLE:
            case MAYBE_ERC20:
                tokenDetailRouter.open(activity, token.getAddress(), token.tokenInfo.symbol, token.tokenInfo.decimals,
                        !token.isEthereum(), defaultWallet.getValue(), token, hasDefinition);
                break;

            case ERC1155:
                tokenDetailRouter.open(activity, token, defaultWallet.getValue(), hasDefinition);
                break;

            case ATTESTATION:
                tokenDetailRouter.openAttestation(activity, token, defaultWallet.getValue(), new NFTAsset((Attestation)token));
                break;

            case ERC721:
            case ERC721_LEGACY:
            case ERC721_TICKET:
            case ERC721_UNDETERMINED:
            case ERC721_ENUMERABLE:
                tokenDetailRouter.open(activity, token, defaultWallet.getValue(), false);
                break;

            case ERC875_LEGACY:
            case ERC875:
                tokenDetailRouter.openLegacyToken(activity, token, defaultWallet.getValue());
                break;

            case NOT_SET:
            case OTHER:
            case DELETED_ACCOUNT:
            case CREATION:
                break;
        }
    }

    public void checkBackup(double fiatValue)
    {
        if (TextUtils.isEmpty(getWalletAddr()) || System.currentTimeMillis() < (lastBackupCheck + BALANCE_BACKUP_CHECK_INTERVAL))
            return;
        lastBackupCheck = System.currentTimeMillis();
        double walletUSDValue = tokensService.convertToUSD(fiatValue);

        if (walletUSDValue > 0.0)
        {
            final BigDecimal calcValue = BigDecimal.valueOf(walletUSDValue);
            genericWalletInteract.getBackupWarning(getWalletAddr())
                    .map(needsBackup -> calculateBackupWarning(needsBackup, calcValue))
                    .subscribeOn(Schedulers.computation())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(backupEvent::postValue, this::onTokenBalanceError).isDisposed();
        }
    }

    private void onTokenBalanceError(Throwable throwable)
    {
        //unable to resolve - phone may be offline
    }

    private GenericWalletInteract.BackupLevel calculateBackupWarning(Boolean needsBackup, @NotNull BigDecimal value)
    {
        if (!needsBackup)
        {
            return GenericWalletInteract.BackupLevel.BACKUP_NOT_REQUIRED;
        }
        else if (value.compareTo(BigDecimal.valueOf(VALUE_THRESHOLD)) >= 0)
        {
            return GenericWalletInteract.BackupLevel.WALLET_HAS_HIGH_VALUE;
        }
        else
        {
            return GenericWalletInteract.BackupLevel.WALLET_HAS_LOW_VALUE;
        }
    }

    public void notifyRefresh()
    {
        tokensService.clearFocusToken(); //ensure if we do a refresh there's no focus token preventing correct update
        tokensService.onWalletRefreshSwipe();
    }

    public boolean isChainToken(long chainId, String tokenAddress)
    {
        return tokensService.isChainToken(chainId, tokenAddress);
    }

    public void calculateFiatValues()
    {
        disposable = tokensService.getFiatValuePair()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(fiatValues::postValue);
    }

    public void showManageWallets(Context context, boolean clearStack)
    {
        manageWalletsRouter.open(context, clearStack);
    }

    public boolean isMarshMallowWarningShown()
    {
        return preferenceRepository.isMarshMallowWarningShown();
    }

    public void setMarshMallowWarning(boolean shown)
    {
        preferenceRepository.setMarshMallowWarning(shown);
    }

    public void saveAvatar(Wallet wallet)
    {
        genericWalletInteract.updateWalletItem(wallet, WalletItem.ENS_AVATAR, () -> {});
    }

    public Intent getBuyIntent(String address)
    {
        Intent intent = new Intent();
        intent.putExtra(C.DAPP_URL_LOAD, onRampRepository.getUri(address, null));
        return intent;
    }

    public MutableLiveData<List<WalletConnectSessionItem>> activeWalletConnectSessions()
    {
        return awWalletConnectClient.sessionItemMutableLiveData();
    }

    public void checkDeleteMetas(TokenCardMeta[] metas)
    {
        List<TokenCardMeta> metasToDelete = new ArrayList<>();
        for (TokenCardMeta meta : metas)
        {
            if (meta.balance.equals("-2"))
            {
                metasToDelete.add(meta);
            }
        }

        if (metasToDelete.size() > 0)
        {
            disposable = tokensService.deleteTokens(metasToDelete)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe();
        }
    }

    private Single<TokenCardMeta[]> getUpdatedTokenMetas()
    {
        return Single.fromCallable(() -> {
            List<TokenCardMeta> tokenMetas = new ArrayList<>();
            try (Realm r = realmManager.getRealmInstance(defaultWallet.getValue()))
            {
                RealmResults<RealmToken> updatedTokens = r.where(RealmToken.class).equalTo("isEnabled", true)
                        .like("address", ADDRESS_FORMAT)
                        .greaterThan("updatedTime", lastTokenFetchTime)
                        .findAll();

                for (RealmToken t : updatedTokens)
                {
                    if (!tokensService.getNetworkFilters().contains(t.getChainId()))
                    {
                        continue;
                    }

                    String balance = TokensRealmSource.convertStringBalance(t.getBalance(), t.getContractType());

                    TokenCardMeta meta = new TokenCardMeta(t.getChainId(), t.getTokenAddress(), balance,
                            t.getUpdateTime(), assetDefinitionService, t.getName(), t.getSymbol(), t.getContractType(),
                            getTokenGroup(t.getChainId(), t.getTokenAddress()));
                    meta.lastTxUpdate = t.getLastTxTime();
                    meta.isEnabled = t.isEnabled();
                    tokenMetas.add(meta);
                    if (t.getBalanceUpdateTime() > lastTokenFetchTime)
                    {
                        lastTokenFetchTime = t.getBalanceUpdateTime() + 1;
                    }
                }
            }

            return tokenMetas.toArray(new TokenCardMeta[0]);
        });
    }

    public void removeTokenMetaItem(String tokenKeyId)
    {
        final String tokenKey = tokenKeyId.endsWith(Attestation.ATTESTATION_SUFFIX) ? tokenKeyId.substring(0, tokenKeyId.length() - Attestation.ATTESTATION_SUFFIX.length())
                : tokenKeyId;

        try (Realm realm = realmManager.getRealmInstance(defaultWallet.getValue()))
        {
            realm.executeTransactionAsync(r -> {
                RealmAttestation realmAttn = r.where(RealmAttestation.class)
                        .equalTo("address", tokenKey)
                        .findFirst();

                if (realmAttn != null)
                {
                    realmAttn.deleteFromRealm();
                }
            });
        }
    }

    public void deleteToken(Token token)
    {
        try (Realm realm = realmManager.getRealmInstance(defaultWallet.getValue()))
        {
            realm.executeTransactionAsync(r -> {
                RealmAttestation realmAttn = r.where(RealmAttestation.class)
                        .equalTo("address", token.getDatabaseKey())
                        .findFirst();

                if (realmAttn != null)
                {
                    realmAttn.deleteFromRealm();
                }
            });
        }
    }

    public void checkRemovedMetas()
    {
        disposable = Single.fromCallable(() -> {
            final List<Token> forRemoval = new ArrayList<>();

            try (Realm realm = realmManager.getRealmInstance(defaultWallet.getValue()))
            {
                RealmResults<RealmAttestation> attnResults = realm.where(RealmAttestation.class)
                        .equalTo("collectionId", ImportAttestation.DELETE_KEY)
                        .findAll();

                for (RealmAttestation attn : attnResults)
                {
                    long chainId = attn.getChains().get(0);
                    Attestation attestationForRemoval = (Attestation) tokensService.getAttestation(chainId, attn.getTokenAddress(), attn.getAttestationID());
                    forRemoval.add(attestationForRemoval);
                }

                if (attnResults.size() > 0)
                {
                    realm.executeTransaction(r -> attnResults.deleteAllFromRealm());
                }
            }

            return forRemoval.toArray(new Token[0]);
        }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(tokensToRemove::postValue);
    }
}
